package bsh;

import java.util.LinkedList;

import org.objectweb.asm.MethodVisitor;

import bsh.asm.acessor.DynamicFieldAccessor;
import bsh.asm.acessor.FieldObjectAcessor;
import bsh.asm.acessor.ObjectAccessor;
import bsh.asm.acessor.ThisObjectAccessor;
import bsh.exception.CompilationAssertionFailed;
import bsh.exception.CompilationFailedException;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;

/**
 * Compilador para um simbolo
 * 
 * @author takeshi
 */
public class AmbiguosNameCompiler implements NodeCompiler {

    private final BSHAmbiguousName node;

    private final CompilationUnit unit;

    /**
     * @param node node
     * @param unit unit
     */
    public AmbiguosNameCompiler(BSHAmbiguousName node, CompilationUnit unit) {
        this.node = node;
        this.unit = unit;
    }

    /**
     * {@inheritDoc}
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        state.addLineNumber(host, this.node.getLineNumber());
        LinkedList<ObjectAccessor> list = this.createLinks(this.node.getLineNumber(), state.currentScope());
        for (ObjectAccessor objectAccessor : list) {
            objectAccessor.loadObject(host, state);
        }
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper nodeResultType(Scope scope) {
        try {
            return this.createLinks(this.node.getLineNumber(), scope).getLast().type();
        } catch (CompilationFailedException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Cria o link para os {@link ObjectAccessor}
     * 
     * @param line numero da linha
     * @param scope Scopo
     * @return {@link LinkedList}
     * @throws CompilationFailedException e
     */
    private LinkedList<ObjectAccessor> createLinks(int line, Scope scope) throws CompilationFailedException {
        return resolveAccessorsFor(line, this.node.text, scope, unit);
    }

    /**
     * @param line numero da linha
     * @param text string
     * @param scope escopo
     * @param unit2 unit
     * @return {@link LinkedList}
     * @throws CompilationFailedException e
     */
    public static LinkedList<ObjectAccessor> resolveAccessorsFor(int line, final String text, Scope scope, CompilationUnit unit2)
            throws CompilationFailedException {
        LinkedList<ObjectAccessor> links = new LinkedList<ObjectAccessor>();
        String[] parts = text.split("[.]");
        ObjectAccessor last = scope.getAccessor(parts[0]);
        if (last == null) {
            TypeWrapper resolved = scope.resolveType(parts[0]);
            if (resolved == null) {
                LinkedList<ObjectAccessor> staticLinks = resolveAccessorsWithType(line, parts, scope, unit2);
                if (staticLinks != null) {
                    return staticLinks;
                }
                if (unit2.options().allowUndefinedVariables()) {
                    last = new DynamicFieldAccessor(new ThisObjectAccessor(unit2.thisType()), unit2.reflectionUtils(), parts[0]);
                } else {
                    throw new CompilationAssertionFailed("[" + line + "] cannot resolve symbol: " + parts[0]);
                }
            } else {
                last = new ClassResolver(resolved);
            }
        }
        links.add(last);
        boolean triedType = false;
        for (int i = 1; i < parts.length; i++) {
            ObjectAccessor current = resolveDependentType(last.type(), parts[i], scope, unit2);
            if (current == null) {
                if (!triedType) {
                    LinkedList<ObjectAccessor> typeResolution = resolveAccessorsWithType(line, parts, scope, unit2);
                    triedType = true;
                    if (typeResolution != null) {
                        return links;
                    }
                }
                current = new DynamicFieldObjectAccessor(parts[i]);
            }
            last = current;
            links.add(current);
        }
        return links;
    }

    /**
     * Tries to resolve a name as a type - either a fully qualified class name or an imported class name.
     * 
     * @param line the declaring line number
     * @param parts the parts of the name
     * @param scope the declaring {@link Scope}
     * @param unit2 the {@link CompilationUnit}
     * @return {@link LinkedList} of {@link ObjectAccessor} or <code>null</code> if it cannot be found
     * @throws CompilationFailedException e
     */
    private static LinkedList<ObjectAccessor> resolveAccessorsWithType(int line, String[] parts, Scope scope, CompilationUnit unit2)
            throws CompilationFailedException {
        // we now iterate until find a proper type
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parts.length - 1; i++) {
            sb.append(parts[i]);
            TypeWrapper wrapper = scope.resolveType(sb.toString());
            if (wrapper != null) {
                // must resolve static links and method calls from here
                return resolveLinksFromStaticType(wrapper, MethodCallCompiler.copyFrom(parts, i + 1), scope, line, unit2);
            }
            sb.append(".");
        }
        return null;
    }

    /**
     * Resolves links from a type - usually fields from a class
     * 
     * @param wrapper the base type
     * @param links the links to follow
     * @param scope the declared scope
     * @param lineNumber int
     * @param unit2 {@link CompilationUnit}
     * @return {@link LinkedList} of {@link ObjectAccessor}
     * @throws CompilationFailedException e
     */
    private static LinkedList<ObjectAccessor> resolveLinksFromStaticType(TypeWrapper wrapper, String[] links, Scope scope, int lineNumber,
            CompilationUnit unit2) throws CompilationFailedException {
        // ok so now we must find a chain of fields ending with a method
        final LinkedList<ObjectAccessor> targetLinks = new LinkedList<ObjectAccessor>();
        ObjectAccessor resolver = new ClassResolver(wrapper);
        targetLinks.add(resolver);
        for (int i = 0; i < links.length; i++) {
            String fieldName = links[i];
            final FieldDescriptor field = MethodCallCompiler.resolveField(resolver.type(), fieldName, unit2);
            if (field == null) {
                throw new CompilationFailedException(lineNumber, "No field named: " + fieldName + " acessible on type: " + resolver.type());
            }
            resolver = new FieldObjectAcessor(field);
            targetLinks.add(resolver);
        }
        return targetLinks;
    }

    /**
     * @param owner Escopo
     * @param name String
     * @param scope escopo
     * @param compilationUnit CompilationUnit
     * @return {@link ObjectAccessor}
     */
    private static ObjectAccessor resolveDependentType(TypeWrapper owner, String name, Scope scope, CompilationUnit compilationUnit) {
        final FieldDescriptor field = owner.getField(name);
        if (field == null) {
            final String suffix = Character.toUpperCase(name.charAt(0)) + name.substring(1);
            final TypeWrapper[] emptyArray = new TypeWrapper[]{};
            MethodDescriptor getter = compilationUnit.reflectionUtils().findMethodForTypes(owner, "get" + suffix, emptyArray);
            if (getter == null) {
                getter = compilationUnit.reflectionUtils().findMethodForTypes(owner, "is" + suffix, emptyArray);
                if (getter != null
                        && !(getter.returnType().equals(JavaClassWrapper.BOOLEAN_WRAPPER) || getter.returnType().equals(
                                JavaClassWrapper.wrapperFor(Boolean.class)))) {
                    // we only consider getter methods with is when the return type is either a boolean or a Boolean
                    getter = null;
                }
            }
            if (getter != null) {
                MethodDescriptor setter =
                        compilationUnit.reflectionUtils().findMethodForTypes(owner, "set" + suffix,
                                new TypeWrapper[]{ getter.returnType() });
                if (setter != null) {
                    // best case scenario
                    return new BeanPropertyObjectAccessor(getter, setter);
                }
                return new BeanPropertyObjectAccessor(getter, name);
            }
            return null;
        }
        return new FieldObjectAcessor(field);
    }

}
